{
 "cells": [
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "# Cross-Layer Equalization\n",
    "\n",
    "This notebook contains an example of how to use AIMET to apply Cross-Layer Equalization (CLE). CLE is a post-training quantization techniques for improving the quantized accuracy of a model. These techniques help recover quantized accuracy when the model quantization is sensitive to parameter quantization as opposed to activation quantization.\n",
    "\n",
    "CLE does not need any data samples. BC may optionally need unlabeled data samples.\n",
    "\n",
    "**Cross-layer equalization**\n",
    "\n",
    "AIMET performs the following steps when running CLE:\n",
    "\n",
    "1. Batch norm (BN) Folding: Folds BN layers into convolution (Conv) layers immediate before or after the Conv layers.\n",
    "2. Cross-layer scaling: For a set of consecutive Conv layers, equalizes the range of tensor values per-channel by scaling their weight tensor values.\n",
    "3. High bias folding: Cross-layer scaling may result in high bias parameter values for some layers. This technique folds some of the bias of a layer into the subsequent layer's parameters.\n",
    "\n",
    "## Overall flow\n",
    "\n",
    "This example performs the following steps:\n",
    "\n",
    "1. Instantiate the example evaluation and training pipeline\n",
    "2. Convert an FP32 PyTorch model to ONNX and evaluate the model's baseline FP32 accuracy\n",
    "3. Create a quantization simulation model (with fake quantization ops inserted) and evaluate this simuation model to get a quantized accuracy score\n",
    "4. Apply CLE and evaluate the simulation model to get a post-finetuned quantized accuracy score\n",
    "\n",
    "<div class=\"alert alert-info\">\n",
    "\n",
    "Note\n",
    "\n",
    "This notebook does not show state-of-the-art results. For example, it uses a relatively quantization-friendly model (Resnet18). Also, some optimization parameters like number of fine-tuning epochs are chosen to improve execution speed in the notebook.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "---\n",
    "\n",
    "## Dataset\n",
    "\n",
    "This example does image classification on the ImageNet dataset. If you already have a version of the data set, use that. Otherwise download the data set, for example from https://image-net.org/challenges/LSVRC/2012/index .\n",
    "\n",
    "<div class=\"alert alert-info\">\n",
    "\n",
    "Note\n",
    "\n",
    "The dataloader provided in this example relies on these features of the ImageNet data set:\n",
    "\n",
    "- Subfolders `train` for the training samples and `val` for the validation samples. See the [pytorch dataset description](https://pytorch.org/vision/0.8/_modules/torchvision/datasets/imagenet.html) for more details.\n",
    "- One subdirectory per class, and one file per image sample.\n",
    "\n",
    "</div>\n",
    "\n",
    "<div class=\"alert alert-info\">\n",
    "\n",
    "Note\n",
    "\n",
    "To speed up the execution of this notebook, you can use a reduced subset of the ImageNet dataset. For example: The entire ILSVRC2012 dataset has 1000 classes, 1000 training samples per class and 50 validation samples per class. However, for the purpose of running this notebook, you can reduce the dataset to, say, two samples per class.\n",
    "\n",
    "</div>\n",
    "\n",
    "Edit the cell below to specify the directory where the downloaded ImageNet dataset is saved."
   ]
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": "DATASET_DIR = '/path/to/dataset/'         # Replace this path with a real directory"
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "---\n",
    "## 1. Instantiate the example training and validation pipeline\n",
    "\n",
    "**Use the following training and validation loop for the image classification task.**\n",
    "\n",
    "Things to note:\n",
    "\n",
    "- AIMET does not put limitations on how the training and validation pipeline is written. AIMET modifies the user's model to create a QuantizationSim model, which is still a PyTorch model. The QuantizationSim model can be used in place of the original model when doing inference or training.\n",
    "- AIMET doesn not put limitations on the interface of the `evaluate()` or `train()` methods. You should be able to use your existing evaluate and train routines as-is.\n"
   ]
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": [
    "import torch\n",
    "import onnxruntime as ort\n",
    "from Examples.common import image_net_config\n",
    "from Examples.onnx.utils.image_net_evaluator import ImageNetEvaluator\n",
    "from Examples.torch.utils.image_net_data_loader import ImageNetDataLoader\n",
    "\n",
    "class ImageNetDataPipeline:\n",
    "\n",
    "    @staticmethod\n",
    "    def get_val_dataloader() -> torch.utils.data.DataLoader:\n",
    "        \"\"\"\n",
    "        Instantiates a validation dataloader for ImageNet dataset and returns it\n",
    "        \"\"\"\n",
    "        data_loader = ImageNetDataLoader(DATASET_DIR,\n",
    "                                         image_size=image_net_config.dataset['image_size'],\n",
    "                                         batch_size=image_net_config.evaluation['batch_size'],\n",
    "                                         is_training=False,\n",
    "                                         num_workers=image_net_config.evaluation['num_workers']).data_loader\n",
    "        return data_loader\n",
    "\n",
    "    @staticmethod\n",
    "    def evaluate(sess: ort.InferenceSession) -> float:\n",
    "        \"\"\"\n",
    "        Given a torch model, evaluates its Top-1 accuracy on the dataset\n",
    "        :param sess: the model to evaluate\n",
    "        \"\"\"\n",
    "        evaluator = ImageNetEvaluator(DATASET_DIR, image_size=image_net_config.dataset['image_size'],\n",
    "                                      batch_size=image_net_config.evaluation['batch_size'],\n",
    "                                      num_workers=image_net_config.evaluation['num_workers'])\n",
    "\n",
    "        return evaluator.evaluate(sess, iterations=None)\n"
   ]
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "---\n",
    "\n",
    "## 2. Convert an FP32 PyTorch model to ONNX, simplify & then evaluate baseline FP32 accuracy"
   ]
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "**2.1 Load a pretrained resnet18 model from torchvision.**\n",
    "\n",
    "You can load any pretrained PyTorch model instead."
   ]
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": [
    "from torchvision.models import resnet18\n",
    "import onnx\n",
    "\n",
    "input_shape = (1, 3, 224, 224)    # Shape for each ImageNet sample is (3 channels) x (224 height) x (224 width)\n",
    "dummy_input = torch.randn(input_shape)\n",
    "filename = \"./resnet18.onnx\"\n",
    "\n",
    "# Load a pretrained ResNet-18 model in torch\n",
    "pt_model = resnet18(pretrained=True)\n"
   ]
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "**2.2 Export the model to ONNX.**"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": [
    "\n",
    "# Export the torch model to onnx\n",
    "torch.onnx.export(pt_model.eval(),\n",
    "                  dummy_input,\n",
    "                  filename,\n",
    "                  export_params=True,\n",
    "                  do_constant_folding=False,\n",
    "                  input_names=['input'],\n",
    "                  output_names=['output'],\n",
    "                  dynamic_axes={\n",
    "                      'input' : {0 : 'batch_size'},\n",
    "                      'output' : {0 : 'batch_size'},\n",
    "                  },\n",
    "                  dynamo=False,\n",
    "                  )\n",
    "\n",
    "model = onnx.load_model(filename)"
   ]
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "---\n",
    "\n",
    "**2.3 It is recommended to simplify the model before using AIMET**"
   ]
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": [
    "from onnxsim import simplify\n",
    "\n",
    "try:\n",
    "    model, _ = simplify(model)\n",
    "except:\n",
    "    print('ONNX Simplifier failed. Proceeding with unsimplified model')"
   ]
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "---\n",
    "\n",
    "**2.4 Decide whether to place the model on a CPU or CUDA device.**\n",
    "\n",
    "This example uses CUDA if it is available. You can change this logic and force a device placement if needed."
   ]
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": [
    "# cudnn_conv_algo_search is fixing it to default to avoid changing in accuracies/outputs at every inference\n",
    "if 'CUDAExecutionProvider' in ort.get_available_providers():\n",
    "    providers = [('CUDAExecutionProvider', {'cudnn_conv_algo_search': 'DEFAULT'}), 'CPUExecutionProvider']\n",
    "else:\n",
    "    providers = ['CPUExecutionProvider']"
   ]
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "---\n",
    "\n",
    "**2.5 Create an ONNX runtime session and compute the floating point 32-bit (FP32) accuracy of this model using the evaluate() routine.**"
   ]
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": [
    "sess = ort.InferenceSession(model.SerializeToString(), providers=providers)\n",
    "accuracy = ImageNetDataPipeline.evaluate(sess)\n",
    "print(accuracy)"
   ]
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "---\n",
    "\n",
    "## 3. Create a quantization simulation model and determine quantized accuracy\n",
    "\n",
    "### Fold Batch Norm layers\n",
    "\n",
    "Before calculating the simulated quantized accuracy using QuantizationSimModel, fold the BatchNorm (BN) layers into adjacent Convolutional layers. The BN layers that cannot be folded are left as they are.\n",
    "\n",
    "BN folding improves inference performance on quantized runtimes but can degrade accuracy on these platforms. This step simulates this on-target drop in accuracy.\n",
    "\n",
    "**3.1 Use the following code to call AIMET to fold the BN layers in-place on the given model.**"
   ]
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": [
    "from aimet_onnx.batch_norm_fold import fold_all_batch_norms_to_weight\n",
    "\n",
    "_ = fold_all_batch_norms_to_weight(model)"
   ]
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "### Create the Quantization Sim Model\n",
    "\n",
    "**3.2 Use AIMET to create a QuantizationSimModel.**\n",
    "\n",
    " In this step, AIMET inserts fake quantization ops in the model graph and configures them.\n",
    "\n",
    "Key parameters:\n",
    "\n",
    "- Setting **default_output_bw** to 8 performs all activation quantizations in the model using integer 8-bit precision\n",
    "- Setting **default_param_bw** to 8 performs all parameter quantizations in the model using integer 8-bit precision\n",
    "- **num_batches** is the number of batches to use to compute encodings. Only five batches are used here for the sake of speed\n",
    "\n",
    "See [QuantizationSimModel in the AIMET API documentation](https://quic.github.io/aimet-pages/releases/latest/apiref/onnx/quantsim.html#apiref-onnx-quantsim) for a full explanation of the parameters."
   ]
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": [
    "from aimet_common.defs import QuantScheme\n",
    "import aimet_onnx\n",
    "from aimet_onnx.quantsim import QuantizationSimModel\n",
    "\n",
    "sim = QuantizationSimModel(model=model,\n",
    "                           quant_scheme=QuantScheme.post_training_tf_enhanced,\n",
    "                           param_type=aimet_onnx.int8,\n",
    "                           activation_type=aimet_onnx.int8,\n",
    "                           providers=providers)"
   ]
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "---\n",
    "AIMET has added quantizer nodes to the model graph, but before the sim model can be used for inference or training, scale and offset quantization parameters must be calculated for each quantizer node by passing unlabeled data samples through the model to collect range statistics. This process is sometimes referred to as calibration. AIMET refers to it as \"computing encodings\".\n",
    "\n",
    "**3.3 Create a routine to pass unlabeled data samples through the model.**\n",
    "\n",
    "The following code is one way to write a routine that passes unlabeled samples through the model to compute encodings. It uses the existing train or validation data loader to extract samples and pass them to the model. Since there is no need to compute loss metrics, it ignores the model output."
   ]
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": [
    "def pass_calibration_data(session):\n",
    "    data_loader = ImageNetDataPipeline.get_val_dataloader()\n",
    "    batch_size = data_loader.batch_size\n",
    "    input_name = sess.get_inputs()[0].name\n",
    "\n",
    "    batch_cntr = 0\n",
    "    for input_data, _ in data_loader:\n",
    "\n",
    "        inputs_batch = input_data.numpy()\n",
    "        session.run(None, {input_name : inputs_batch})\n",
    "\n",
    "        batch_cntr += 1\n",
    "        # Use 10000 samples for computing initial scale/offset\n",
    "        if (batch_cntr * batch_size) > 1000:\n",
    "            break"
   ]
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "A few notes regarding the data samples:\n",
    "\n",
    "- A very small percentage of the data samples are needed. For example, the training dataset for ImageNet has 1M samples; 500 or 1000 suffice to compute encodings.\n",
    "- The samples should be reasonably well distributed. While it's not necessary to cover all classes, avoid extreme scenarios like using only dark or only light samples. That is, using only pictures captured at night, say, could skew the results.\n",
    "\n",
    "---\n",
    "\n",
    "**3.4 Call AIMET to use the routine to pass data through the model and compute the quantization encodings.**\n",
    "\n",
    "Encodings here refer to scale and offset quantization parameters."
   ]
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": "sim.compute_encodings(forward_pass_callback=pass_calibration_data)"
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "---\n",
    "\n",
    "The QuantizationSim model is now ready to be used for inference or training.\n",
    "\n",
    "**3.5 Pass the model to the same evaluation routine as before to calculate a simulated quantized accuracy score for INT8 quantization for comparison with the FP32 score.**"
   ]
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": [
    "accuracy = ImageNetDataPipeline.evaluate(sim.session)\n",
    "print(accuracy)"
   ]
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "---\n",
    "## 4. Apply CLE\n",
    "\n",
    "**4.1 Perform CLE**\n",
    "\n",
    "The next cell performs cross-layer equalization on the model. As noted before, the function folds batch norms, applies cross-layer scaling, and then folds high biases.\n",
    "\n",
    "<div class=\"alert alert-info\">\n",
    "\n",
    "Note\n",
    "\n",
    "The CLE procedure needs BN statistics. If a BN folded model is provided, CLE runs the cross-layer scaling (CLS) optimization step but skips the high-bias absorption (HBA) step. To avoid this, load the original model again before running CLE.\n",
    "\n",
    "</div>\n",
    "\n",
    "<div class=\"alert alert-info\">\n",
    "\n",
    "Note\n",
    "\n",
    "CLE equalizes the model in-place.\n",
    "\n",
    "</div>\n",
    "\n"
   ]
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": [
    "filename = \"./resnet18.onnx\"\n",
    "model = onnx.load_model(filename)"
   ]
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "**It is recommended to simplify the model before using AIMET**"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": [
    "from onnxsim import simplify\n",
    "\n",
    "try:\n",
    "    model, _ = simplify(model)\n",
    "except:\n",
    "    print('ONNX Simplifier failed. Proceeding with unsimplified model')"
   ]
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": [
    "from aimet_onnx.cross_layer_equalization import equalize_model\n",
    "\n",
    "equalize_model(model)"
   ]
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "---\n",
    "\n",
    "**4.2 Compute the accuracy of the equalized model.**\n",
    "\n",
    "Create a simulation model as before and evaluate it to determine simulated quantized accuracy."
   ]
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": [
    "sim = QuantizationSimModel(model=model,\n",
    "                           quant_scheme=QuantScheme.post_training_tf_enhanced,\n",
    "                           param_type=aimet_onnx.int8,\n",
    "                           activation_type=aimet_onnx.int8,\n",
    "                           providers=providers)\n",
    "\n",
    "sim.compute_encodings(forward_pass_callback=pass_calibration_data)\n",
    "\n",
    "accuracy = ImageNetDataPipeline.evaluate(sim.session)\n",
    "print(accuracy)"
   ]
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "## For more information\n",
    "\n",
    "See the [AIMET API docs](https://quic.github.io/aimet-pages/releases/latest/apiref/index.html) for details about the AIMET APIs and optional parameters.\n",
    "\n",
    "See the [other example notebooks](https://github.com/quic/aimet/tree/develop/Examples/torch/quantization) to learn how to use other AIMET post-training quantization techniques.\n",
    "\n",
    "To learn more about these techniques, see [\"Data-Free Quantization Through Weight Equalization and Bias Correction\"](https://arxiv.org/abs/1906.04721) from ICCV 2019."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
